/* Copyright (c) 2012 William Steven Knauer
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software
 * and associated documentation files (the "Software"), to deal in the Software without restriction,
 * including without limitation the rights to use, copy, modify, merge, publish, distribute,
 * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all copies or
 * substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
 * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
 * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

package com.williamknauer.android.yagol;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;

import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;

import android.content.Context;
import android.graphics.Color;
import android.opengl.GLES20;
import android.opengl.GLSurfaceView.Renderer;
import android.opengl.GLU;
import android.opengl.Matrix;
import android.os.SystemClock;

/**
 * OpenGL ES 2.0 Renderer for the game.
 * 
 * @author William S Knauer <knauerw1@gmail.com>
 * @version 2012.0111
 */
public class GameRenderer2 implements Renderer {
	private final float[] mModelMatrix;
	private final float[] mViewMatrix;
	private final float[] mProjMatrix;
	private final float[] mModelViewMatrix;
	private final float[] mModelViewProjMatrix;
	private final float[] mTranslationMatrix;
	private final int[] mViewport;
	private final float[] mWorldCoords;

	private FloatBuffer mCellVertexBuffer;
	private int mCellProgram;

	private int maCellPositionHandle;
	private int muMVPMatrixHandle;
	private int muTMatrixHandle;
	private int muFadeinTimeoutHandle;
	private int muFadeinDurationHandle;
	private int muFadeoutDurationHandle;
	private int muColorHandle;
	private int muFadeColorHandle;
	private int muBackgroundColorHandle;
	private int muAgeHandle;
	private int muFAliveHandle;
	private int muVAliveHandle;

	private final GameModel mGameModel;
	private final Context mContext;
	private final int mRows;
	private final int mCols;
	private final float mWidth;
	private final float mHeight;
	private final float mTop;
	private final float mLeft;

	public GameRenderer2(GameModel gameModel, Context context) {
		/*
		 * Integrate the GameModel.
		 */
		mGameModel = gameModel;
		mContext = context;
		mRows = gameModel.getRows();
		mCols = gameModel.getCols();
		mWidth = CELL_W * mCols + CELL_SH * (mCols - 1);
		mHeight = CELL_H * mRows + CELL_SV * (mRows - 1);
		mLeft = -mWidth / 2.0f;
		mTop = mHeight / 2.0f;

		/*
		 * Initialize OpenGL matrices.
		 */
		mModelMatrix = new float[16];
		Matrix.setIdentityM(mModelMatrix, 0);
		mViewMatrix = new float[16];
		Matrix.setIdentityM(mViewMatrix, 0);
		mProjMatrix = new float[16];
		Matrix.setIdentityM(mProjMatrix, 0);
		mModelViewMatrix = new float[16];
		Matrix.setIdentityM(mModelViewMatrix, 0);
		mModelViewProjMatrix = new float[16];
		Matrix.setIdentityM(mModelViewProjMatrix, 0);

		/*
		 * Initialize cell translation matrix.
		 */
		mTranslationMatrix = new float[16];
		Matrix.setIdentityM(mTranslationMatrix, 0);

		/*
		 * Initialize viewport array.
		 */
		mViewport = new int[4];

		/*
		 * Initialize world coordinate temp storage array.
		 */
		mWorldCoords = new float[4];
	}

	@Override
	public void onDrawFrame(GL10 gl) {
		/*
		 * Set the frame background color.
		 */
		int clearColor = mGameModel.getPrefColorBackground();
		GLES20.glClearColor(Color.red(clearColor) / 255.0f, Color.green(clearColor) / 255.0f,
				Color.blue(clearColor) / 255.0f, Color.alpha(clearColor) / 255.0f);

		/*
		 * Redraw background color.
		 */
		GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);

		/*
		 * Load the shader program.
		 */
		GLES20.glUseProgram(mCellProgram);

		/*
		 * Apply Model-View-Projection matrix to the rendering pipeline.
		 */
		Matrix.multiplyMM(mModelViewMatrix, 0, mViewMatrix, 0, mModelMatrix, 0);
		Matrix.multiplyMM(mModelViewProjMatrix, 0, mProjMatrix, 0, mModelViewMatrix, 0);
		GLES20.glUniformMatrix4fv(muMVPMatrixHandle, 1, false, mModelViewProjMatrix, 0);

		/*
		 * Apply static uniforms to the rendering pipeline.
		 */
		GLES20.glUniform1f(muFadeinTimeoutHandle, mGameModel.getPrefFadeinTimeout());
		GLES20.glUniform1f(muFadeinDurationHandle, mGameModel.getPrefFadeinDuration());
		GLES20.glUniform1f(muFadeoutDurationHandle, mGameModel.getPrefFadeoutDuration());
		int color = mGameModel.getPrefColorCell();
		GLES20.glUniform4f(muColorHandle, Color.red(color) / 255.0f, Color.green(color) / 255.0f,
				Color.blue(color) / 255.0f, Color.alpha(color) / 255.0f);
		int fadeColor = mGameModel.getPrefColorFadein();
		GLES20.glUniform4f(muFadeColorHandle, Color.red(fadeColor) / 255.0f,
				Color.green(fadeColor) / 255.0f, Color.blue(fadeColor) / 255.0f,
				Color.alpha(fadeColor) / 255.0f);
		int backgroundColor = mGameModel.getPrefColorBackground();
		GLES20.glUniform4f(muBackgroundColorHandle, Color.red(backgroundColor) / 255.0f,
				Color.green(backgroundColor) / 255.0f, Color.blue(backgroundColor) / 255.0f,
				Color.alpha(backgroundColor) / 255.0f);

		/*
		 * Prepare the cell vertex data.
		 */
		GLES20.glVertexAttribPointer(maCellPositionHandle, 3, GLES20.GL_FLOAT, false, 0,
				mCellVertexBuffer);
		GLES20.glEnableVertexAttribArray(maCellPositionHandle);

		/*
		 * Get the time so we can determine the age of each cell.
		 */
		int time = (int) (SystemClock.uptimeMillis() - GameModel.EPOCH);
		int fadeoutDuration = mGameModel.getPrefFadeoutDuration();

		/*
		 * Draw each living/recently dead each cell...
		 */
		for (int i = 0; i < mRows * mCols; i++) {
			// cell alive, cell dead
			boolean cell = mGameModel.getCell(i);
			int age = time - mGameModel.getCellTime(i);

			if (cell == GameModel.CELL_ALIVE || age <= fadeoutDuration) {
				/*
				 * Determine the cell's translation from (left,top)=(0,0).
				 */
				mTranslationMatrix[12] = mLeft + (i % mCols) * (CELL_W + CELL_SH);
				mTranslationMatrix[13] = mTop - (i / mCols) * (CELL_H + CELL_SV);

				/*
				 * Apply the cell's translation matrix to the rendering
				 * pipeline.
				 */
				GLES20.glUniformMatrix4fv(muTMatrixHandle, 1, false, mTranslationMatrix, 0);

				/*
				 * Apply the cell to the rendering pipeline.
				 */
				GLES20.glUniform1f(muAgeHandle, age);
				GLES20.glUniform1i(muVAliveHandle, (cell == GameModel.CELL_ALIVE) ? 1 : -1);
				GLES20.glUniform1i(muFAliveHandle, (cell == GameModel.CELL_ALIVE) ? 1 : -1);

				/*
				 * Draw the cell.
				 */
				GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
			}
		}
	}

	@Override
	public void onSurfaceChanged(GL10 gl, int width, int height) {
		/*
		 * Reset the viewport.
		 */
		GLES20.glViewport(0, 0, width, height);
		mViewport[2] = width;
		mViewport[3] = height;

		/*
		 * Set the projection matrix to counter the new aspect ratio.
		 */
		float ratio = width / (float) height;
		Matrix.frustumM(mProjMatrix, 0, -ratio, ratio, -1.0f, 1.0f, 3.0f, 7.0f);

		/*
		 * Get handles to uniform variables.
		 */
		muMVPMatrixHandle = GLES20.glGetUniformLocation(mCellProgram, "uMVPMatrix");
		muTMatrixHandle = GLES20.glGetUniformLocation(mCellProgram, "uTMatrix");
		muAgeHandle = GLES20.glGetUniformLocation(mCellProgram, "age");
		muFadeinTimeoutHandle = GLES20.glGetUniformLocation(mCellProgram, "fadeinTimeout");
		muFadeinDurationHandle = GLES20.glGetUniformLocation(mCellProgram, "fadeinDuration");
		muFadeoutDurationHandle = GLES20.glGetUniformLocation(mCellProgram, "fadeoutDuration");
		muColorHandle = GLES20.glGetUniformLocation(mCellProgram, "color");
		muFadeColorHandle = GLES20.glGetUniformLocation(mCellProgram, "fadeColor");
		muBackgroundColorHandle = GLES20.glGetUniformLocation(mCellProgram, "backgroundColor");
		muFAliveHandle = GLES20.glGetUniformLocation(mCellProgram, "fAlive");
		muVAliveHandle = GLES20.glGetUniformLocation(mCellProgram, "vAlive");

		/*
		 * Set the camera view matrix.
		 */
		Matrix.setLookAtM(mViewMatrix, 0, 0.0f, 0.0f, 3.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f);
	}

	@Override
	public void onSurfaceCreated(GL10 gl, EGLConfig config) {
		/*
		 * Set the frame background color.
		 */
		int backgroundColor = mGameModel.getPrefColorBackground();
		GLES20.glClearColor(Color.red(backgroundColor) / 255.0f,
				Color.green(backgroundColor) / 255.0f, Color.blue(backgroundColor) / 255.0f,
				Color.alpha(backgroundColor) / 255.0f);

		/*
		 * Set up the shader programs.
		 */
		mCellProgram = GLES20.glCreateProgram();
		int cellVertexShader = GLES20.glCreateShader(GLES20.GL_VERTEX_SHADER);
		GLES20.glShaderSource(cellVertexShader, getAsset("shaders/cell.vert"));
		GLES20.glCompileShader(cellVertexShader);
		GLES20.glAttachShader(mCellProgram, cellVertexShader);
		int cellFragmentShader = GLES20.glCreateShader(GLES20.GL_FRAGMENT_SHADER);
		GLES20.glShaderSource(cellFragmentShader, getAsset("shaders/cell.frag"));
		GLES20.glCompileShader(cellFragmentShader);
		GLES20.glAttachShader(mCellProgram, cellFragmentShader);
		GLES20.glLinkProgram(mCellProgram);

		/*
		 * Get a handle to the shaders' attributes.
		 */
		maCellPositionHandle = GLES20.glGetAttribLocation(mCellProgram, "vPosition");

		/*
		 * Initialize vertex buffers.
		 */
		initVertexBuffers();
	}

	private void initVertexBuffers() {
		/*
		 * Allocate direct buffer through java.nio.
		 */
		ByteBuffer cvbb = ByteBuffer.allocateDirect(sCellVertices.length * 4);
		cvbb.order(ByteOrder.nativeOrder());
		mCellVertexBuffer = cvbb.asFloatBuffer();

		/*
		 * Populate the newly allocated buffer with vertex data and reset the
		 * buffer's position.
		 */
		mCellVertexBuffer.put(sCellVertices).position(0);
	}

	public boolean handleScroll(float x, float y, float dx, float dy) {
		/*
		 * Store previous x, y in dx, dy.
		 */
		dx += x;
		dy += y;

		/*
		 * Convert window coordinates to world coordinates.
		 */
		findWorldCoordinates(x, y);
		x = mWorldCoords[0];
		y = mWorldCoords[1];
		findWorldCoordinates(dx, dy);
		dx = mWorldCoords[0] - x;
		dy = mWorldCoords[1] - y;

		/*
		 * Apply the transformation to the model matrix.
		 */
		Matrix.translateM(mModelMatrix, 0, dx, dy, 0);
		return true;
	}

	public boolean handleScale(float focusX, float focusY, float scale) {
		/*
		 * Convert window coordinates to world coordinates.
		 */
		findWorldCoordinates(focusX, focusY);
		focusX = mWorldCoords[0];
		focusY = mWorldCoords[1];

		/*
		 * Apply the transformation to the model matrix.
		 */
		Matrix.scaleM(mModelMatrix, 0, scale, scale, scale);
		Matrix.translateM(mModelMatrix, 0, focusX - focusX * scale, focusY - focusY * scale, 0);
		return true;
	}

	private void findWorldCoordinates(float windowX, float windowY) {
		/*
		 * Convert window coordinates into OpenGL world coordinates.
		 */
		windowY = mViewport[3] - windowY;
		FloatBuffer windowZBuf = FloatBuffer.wrap(new float[1]);
		GLES20.glReadPixels((int) windowX, (int) windowY, 1, 1, GLES20.GL_DEPTH_COMPONENT,
				GLES20.GL_FLOAT, windowZBuf);
		GLU.gluUnProject(windowX, windowY, windowZBuf.get(), mModelViewMatrix, 0, mProjMatrix, 0,
				mViewport, 0, mWorldCoords, 0);
		for (int i = 0; i < mWorldCoords.length; i++) {
			mWorldCoords[i] *= EYE_Z;
		}
	}

	public boolean handleSingleTap(float windowX, float windowY) {
		/*
		 * Convert window coordinates into OpenGL world coordinates.
		 */
		findWorldCoordinates(windowX, windowY);

		/*
		 * Use the world coordinates to find the index of the tapped cell.
		 */
		mWorldCoords[0] += mWidth / 2 + CELL_SH / 2;
		mWorldCoords[1] += mHeight / 2 - CELL_SV / 2;
		mWorldCoords[1] = mHeight - mWorldCoords[1];
		int row = (int) Math.floor((mWorldCoords[1] / (CELL_H + CELL_SV)));
		int col = (int) Math.floor((mWorldCoords[0] / (CELL_W + CELL_SH)));

		/*
		 * Flip the tapped cell if it exists.
		 */
		if ((row >= 0 && row < mRows) && (col >= 0 && col < mCols)) {
			mGameModel.flip(row * mCols + col);
			return true;
		} else {
			return false;
		}
	}

	public String getAsset(String assetName) {
		/*
		 * Load a file from the assets folder into a string.
		 */
		try {
			InputStream is = mContext.getAssets().open(assetName);
			StringBuilder sb = new StringBuilder();
			int c = is.read();
			while (c != -1) {
				sb.append((char) c);
				c = is.read();
			}
			return sb.toString();
		} catch (FileNotFoundException e) {
			e.printStackTrace();
			return null;
		} catch (IOException e) {
			e.printStackTrace();
			return null;
		}
	}

	private static final float CELL_W = 0.5f;
	private static final float CELL_H = 0.5f;
	private static final float CELL_SH = 0.0f;
	private static final float CELL_SV = 0.0f;
	private static final float EYE_Z = 3.0f;

	private static final float[] sCellVertices = { 0.0f, 0.0f, 0.0f, CELL_W, 0.0f, 0.0f, 0.0f,
			-CELL_H, 0.0f, CELL_W, -CELL_H, 0.0f };
}
